package org.carabiner.mockutil;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import junit.framework.AssertionFailedError;

import org.jmock.core.Invocation;
import org.jmock.core.Stub;

/**
 * <code>DynamicStub</code> is an alternative to writing concrete stub classes
 * for unit tests. It allows you to stub only the methods you need as an inner
 * class, and it has the benefit of removing interdependencies from unit tests.
 * 
 * The intended use of the <code>DynamicStub</code> is to stub methods using
 * inner classes. For example: <code><pre>
 *     interface DummyInterface
 *     {
 *       public String join(String one, String two);
 *     }
 *  
 *     dummyMock.expects(atLeastOnce()).will(new DynamicStub(DummyInterface.class.getName())
 *     {
 *       public String join(String one, String two)
 *       {
 *         return (one + two);
 *       }
 *     });
 *     dummy = (DummyInterface) dummyMock.proxy();
 *     String result = dummy.join(&quot;Hello &quot;, &quot;World&quot;);
 *     assertEquals(&quot;The stub for join() didn't work as expected.&quot;, &quot;Hello World&quot;, result);
 * </pre></code>
 * 
 * <b>See</b> {@link TestDynamicStub} for usage examples.
 * 
 * @author <a href="mailto:bignate@gmail.com">Nathan Eror</a>
 * @version 0.3
 */
public class DynamicStub implements Stub
{
  private String name;

  /**
   * Create a <code>DynamicStub</code> with the default
   * <code>displayName</code> which is the fully qualified class name.
   */
  public DynamicStub()
  {
    name = getClass().getName();
  }

  /**
   * Create a <code>DynamicStub</code> with the name <code>displayName</code>.
   * The <code>displayName</code> is used by jMock when it reports assertion
   * failures.
   * 
   * @param displayName
   *          String
   */
  public DynamicStub(String displayName)
  {
    this();
    if (displayName != null)
    {
      name = displayName;
    }
  }

  /**
   * {@inheritDoc}
   * 
   * @param stringBuffer
   *          StringBuffer
   * @return StringBuffer
   */
  public StringBuffer describeTo(StringBuffer stringBuffer)
  {
    stringBuffer.append(name);
    return stringBuffer;
  }

  /**
   * {@inheritDoc}
   * 
   * @param invocation
   *          Invocation
   * @return Object
   * @throws Throwable
   */
  public Object invoke(Invocation invocation) throws Throwable
  {
    Method stubbedMethod = findStubbedMethod(invocation.invokedMethod);
    if (stubbedMethod == null)
    {
      throw new AssertionFailedError("Invoked method "
          + invocation.invokedMethod.getName()
          + " was not expected in this mock.");
    }
    stubbedMethod.setAccessible(true);
    try
    {
      return stubbedMethod.invoke(this, invocation.parameterValues.toArray());
    } catch (InvocationTargetException ex)
    {
      throw new StubInvocationException(invocation, ex);
    } catch (IllegalAccessException ex)
    {
      throw new StubInvocationException(invocation, ex);
    }
  }

  private Method findStubbedMethod(Method calledMethod)
  {
    Method stubbedMethod = null;
    Method[] methods = getClass().getDeclaredMethods();
    for (int i = 0; i < methods.length; i++)
    {
      Method method = methods[i];
      if (method.getName().equals(calledMethod.getName())
          && matchParameters(method.getParameterTypes(), calledMethod
              .getParameterTypes()))
      {
        stubbedMethod = method;
      }
    }
    return stubbedMethod;
  }

  private boolean matchParameters(Class[] declaredParameters,
      Class[] calledParameters)
  {
    boolean paramsMatch = false;
    if (declaredParameters.length == 0 && calledParameters.length == 0)
    {
      paramsMatch = true;
    } else if (declaredParameters.length == calledParameters.length)
    {
      for (int i = 0; i < declaredParameters.length; i++)
      {
        paramsMatch = (declaredParameters[i].equals(calledParameters[i])) ? true
            : false;
      }
    }
    return paramsMatch;
  }

  public boolean canInvoke(Invocation invocation)
  {
    Method method = findStubbedMethod(invocation.invokedMethod);
    return (method == null) ? false : true;
  }

  public String toString()
  {
    return name;
  }
}
